CQRS y Proyecciones
Los agregados están diseñados para escritura: garantizan invariantes, gestionan transacciones, registran eventos. Pero no son necesariamente la representación óptima para leer datos.
Cuando el módulo de descripciones necesita mostrar una lista de descripciones con el nombre legible de cada recurso, necesita combinar datos de múltiples tablas. Sin BCs, haría JOINs entre módulos. Con BCs, no puede hacer esos JOINs. ¿Cómo resuelve el conflicto?
La respuesta es CQRS (Command Query Responsibility Segregation): usar modelos distintos para escritura y para lectura.
CQRS: separar escritura y lectura
La idea central de CQRS es sencilla: el modelo que garantiza la consistencia en escritura (el agregado) no tiene por qué ser el modelo que se usa para leer. Se pueden tener dos representaciones distintas del mismo concepto:
| Modelo | Propósito | Características |
|---|---|---|
| Write Model | Garantizar invariantes, registrar eventos | Tiene lógica de negocio, reglas, validaciones |
| Read Model | Devolver datos al frontend con eficiencia | Casi un DTO, sin lógica, optimizado para lectura |
El Write Model es la fuente de verdad. El Read Model es una proyección — una vista desnormalizada calculada a partir de los eventos del Write Model.
Proyecciones: vistas materializadas gestionadas por la aplicación
Una proyección es una vista materializada cuya lógica de materialización recae sobre la aplicación, no sobre la base de datos.
El flujo de una proyección event-driven:
Write Model (módulo de variables)
│
│ 1. VariableCreated { id: 42, name: "T_HORNO_PRINCIPAL" }
│
▼
EventBus
│
│ 2. Listener escucha el evento
│
▼
Read Model (módulo de descripciones)
│
│ 3. INSERT INTO module_description_variable (id=42, name='T_HORNO_PRINCIPAL')
│
▼
module_description_variable (tabla mirror = proyección)
El cálculo pesado ocurre en el momento del INSERT (escritura), no del SELECT (lectura). El GET posterior es un SELECT * FROM module_description_variable WHERE id = ? sin JOINs.
El patrón mirror del módulo de descripciones (presentado en el capítulo 3) es exactamente una proyección event-driven. Cada tabla mirror es el Read Model del módulo de descripciones sobre un recurso de otro BC:
| Tabla mirror | BC de origen | Evento que la actualiza |
|---|---|---|
module_description_variable | Módulo de variables | VariableCreated, VariableChanged, VariableRemoved |
module_description_group | Módulo de grupos | GroupCreated, GroupChanged, GroupRemoved |
module_description_user | Módulo de usuarios | UserCreated, UserChanged, UserRemoved |
| ... | ... | ... |
Cada tabla contiene solo {id, name} — exactamente lo que el módulo de descripciones necesita del recurso externo para funcionar de forma autónoma. Nada más.
Las cuatro alternativas para evitar JOINs
Sin embargo las proyecciones no son la única solución al problema de los JOINs entre módulos. Hay cuatro enfoques, todos ellos presentes dentro de DWall
| Alternativa | Descripción | Cuándo usarla |
|---|---|---|
| Repository como anticorrupción | El repositorio ejecuta JOINs internamente, ocultos al dominio | Inicio del proyecto, datos poco voluminosos |
| Vistas lógicas de BD | CREATE VIEW que absorbe los JOINs | Dominio simple, rendimiento no crítico |
| Vistas materializadas | Pre-computan los JOINs en BD | Cuando la BD (PostgreSQL) puede gestionar el refresh |
| Proyecciones event-driven | La aplicación mantiene el Read Model | Alta carga de lectura, BCs bien definidos |
DWall usa proyecciones event-driven para los mirrors del módulo de descripciones. La elección es coherente con la arquitectura de BCs: evitas JOINs cruzados entre módulos y tampoco requieres la infraestructura de vistas materializadas.
Sin embargo, tal como veremos en el capítulo de la base de datos vectorial, nada de esto es blanco y negro. La elección puede depender del volumen de datos, del coste de mantener los listeners y del grado de consistencia requerido — y existen casos en las opciones menos puristas resultan más convenientes.
Read Model: casi un DTO
El objeto que devuelve el módulo de descripciones para el frontend — el NamedDescription con descripción, nombre del recurso, tipo, usuario y timestamp — es prácticamente un DTO. No tiene lógica de negocio. No registra eventos. No garantiza invariantes.
Esto es correcto. Un Read Model no necesita ser un agregado. Su único trabajo es estar disponible para ser leído eficientemente.
Write: Description.create(...) → guarda en module_description_description
→ registra DescriptionCreated
Read: NamedDescription → join entre description + mirror (solo id+name)
→ nada de eventos, nada de lógica
La distinción no requiere carpetas separadas read-model/ y write-model/. Se entiende de forma inherente: la fuente de verdad (descripción principal) es el Write Model; las tablas mirror que sirven al frontend son el Read Model.